/**
 * 
 */
package com.googlecode.cswish.struts.spring;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.components.TextField;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.springframework.transaction.annotation.Transactional;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.annotation.ParamScope;
import com.googlecode.cswish.annotation.Protected;
import com.googlecode.cswish.annotation.ScopeValue;
import com.googlecode.cswish.exception.AppException;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.model.PageInfo;
import com.googlecode.cswish.model.Reference;
import com.googlecode.cswish.oosearch.Condition;
import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.oosearch.QLParse;
import com.googlecode.cswish.security.Context;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.hibernate.IDynamicModel;
import com.googlecode.cswish.util.BlankObject;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.OgnlExUtil;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.ValueStack;

/**
 * It provide the basic CRUD operations and OO search<br>
 * 
 * @author Jerry.Feng Date: 2008-3-30
 */
//@Service
@Transactional
public class GenericJPAService implements GenericService {
	private final static Logger logger = Logger.getLogger(GenericJPAService.class);
	
	@PersistenceContext
	protected EntityManager em;
	
	@Resource
	protected QLParse qlParse;
		
	@Resource
	protected ObjectFactory objectFactory;
	
	@Resource
	protected BeanIntrospector beanIntrospector;

	@Resource
	protected OgnlExUtil ognlUtil;
	
	@Resource
	protected Config config;
	
	@Resource
	protected SafeManager safeManager;

    /**
     * Search one page of data
     * 
     * @param model
     * @param pageNo
     * @param newSearch
     * @return
     */
    @SuppressWarnings("unchecked")
    @ParamScope(scope=ScopeValue.SESSION)
    public <T> PageInfo<T> search(
    		@GenericType T model, String keywords,
    		int start, int limit, String sort, String dir, boolean newSearch,
    		Reference<QLInfo> qlInfoRef, @ParamScope(visible=false, value="#ps")PageElements ps) {   	
		QLInfo qlInfo = buildQlInfo(model, keywords, start, limit, sort, dir, newSearch, qlInfoRef, ps);
		
		PageInfo<T> pageInfo = null;
		int count = getCount(qlInfo, false);
    	if (count > 0 && qlInfo.getFirstResult() < count) {
    		pageInfo = searchByQl(qlInfo, false);
    		pageInfo.setTotalCount(count);
    	} else {
    		pageInfo = new PageInfo<T>(config.getPageSize());
    		pageInfo.setData(new ArrayList<T>(0));
    	}
		return pageInfo;
	}

    @Protected
	public <T> QLInfo buildQlInfo(T model, String keywords, int start, int limit, String sort, String dir,
			boolean newSearch, Reference<QLInfo> qlInfoRef, PageElements ps) {
		// get it from pageInfo
		QLInfo qlInfo = qlInfoRef == null ? null : qlInfoRef.get();
		if (newSearch || qlInfo == null) {
			if (StringUtils.isNotEmpty(keywords)) {
				// FIXME: use full search to replace the following code
				String className = model == null ? ClassUtil.getRuntimeType(GenericType.MODEL)
						: model.getClass().getName();
	    		StringBuilder ql = new StringBuilder(" from " + className + " a ");
	    		List<Object> parameters = new ArrayList<Object>();
	    		String paramValue = keywords + '%';
	    		boolean isFirst = true;
	    		for (String fieldName : ps.getModelFields()) {
	    			boolean isString = TextField.class.getName().equals(ps.getElement(fieldName).getType());
	    			if (isString) {
		    			if (isFirst) {
		    				isFirst = false;
		    				ql.append(" where ");
		    			} else {
		    				ql.append(" or ");
		    			}
		    			// "execute" --> "a"
		    			
		    			ql.append("a").append(fieldName.substring(7)).append(" like ?");
		    			parameters.add(paramValue);
	    			}
	    		}
	    		qlInfo = new QLInfo(ql.toString(), parameters);
	    		
//	    		return searchFulltext(model, condition);
	    	} else {
				if (model == null) {
					// try to initialize the model according the request action name
					model = (T) objectFactory.buildBean(GenericType.MODEL, Object.class, null);
				}
				qlInfo = qlParse.getSelectHQL(model, Condition.DEFAULT);	
	    	}
			if (qlInfoRef != null) {
				qlInfoRef.set(qlInfo);
			}
		}
		
		if (start == 0 && limit == 0) {
			limit = config.getPageSize();
		} else if (limit - start > config.getPageSize() * 10) {
			logger.warn("Search too much, there're performance problems! limit the total size to ten times of config.pageSize");
			limit = start + config.getPageSize() * 10;
		}
		qlInfo.setFirstResult(start);
		qlInfo.setMaxResults(limit);
		qlInfo.setSort(sort);
		qlInfo.setDir(dir);
		return qlInfo;
	}
    
    
    
    /**
     * Convert model to QL expression and return the table result
     * 
     * @param model
     * @param condition
     * @return
     */
    @Protected
    public <T> PageInfo<T> searchByModel(T model, Condition condition) {
    	PageInfo<T> pageInfo = null;
    	if (model != null) {
	    	QLInfo qlInfo = qlParse.getFromClause(model, condition);
	    	int count = getCount(qlInfo, false);
	    	
	    	if (count > 0 && condition.getOffset() < count) {
	    		pageInfo = searchByQl(qlParse.toSelectHQL(qlInfo), false);
	    		pageInfo.setTotalCount(count);
	    	}
    	} else {
    		logger.debug("parameter 'model' is null");
    	}
    	
    	if (pageInfo == null) {
    		pageInfo = new PageInfo<T>(config.getPageSize());
    		pageInfo.setData(new ArrayList<T>(0));
    	}
    	return pageInfo;
    }
    
    private void addPermissionFilter(QLInfo qlInfo) {
    	if (qlInfo.isWithPermissionFilter() && safeManager.isEnablePermissionCheck()) {
    		return;
    	}
    	
    	HttpServletRequest request = ServletActionContext.getRequest();
    	UserInfo userInfo = safeManager.getLoginUser(request);
    	if (userInfo == null) {
    		return ;
    	}
    	
    	String userId = userInfo.getLoginId();
    	String oldQl = qlInfo.getQl();
    	// from modelName a where
    	int modelBegin = oldQl.indexOf("from ") + 5;
    	if (modelBegin < 5) {
    		// wrong QL?
    		modelBegin = 0;
    	}
    	int modelEnd = oldQl.indexOf(' ', modelBegin + 1);
    	if (modelEnd == -1) {
    		modelEnd = oldQl.length();
    	}
    	String modelPath = oldQl.substring(modelBegin, modelEnd);
    	Context context = new Context(ServletActionContext.getValueStack(request));
    	String filterCondition = safeManager.checkResult(userId, modelPath, context);
    	if (filterCondition != null) {
    		boolean hasWhereCondition = oldQl.indexOf(" where ") != -1;
    		String ql;
    		int end = oldQl.lastIndexOf(" order by ");
    		if (end == -1) {
    			ql = oldQl + (hasWhereCondition ? " and " : " where ") + filterCondition;
    		} else {
    			ql = oldQl.substring(0, end) + (hasWhereCondition ? " and " : " where ") + filterCondition + oldQl.substring(end);
    		}
    		qlInfo.setQl(ql);
    	}
    	qlInfo.setWithPermissionFilter(true);
    }
    
    @Protected
    public <T> PageInfo<T> searchFulltext(T model, Condition condition) {
    	PageInfo<T> pageInfo = null;
    	
    	if (model != null) {
	    	QLInfo qlInfo = qlParse.getFromClause(model, condition);
	    	
	    	FullTextEntityManager fullTextEm = Search.getFullTextEntityManager(em);
	    	BooleanQuery bq = new BooleanQuery();
	
	    	int index = 0;
	        for (String paramName : qlInfo.getParamNames()) {
	           TermQuery tq = new TermQuery(new Term(paramName, qlInfo.getParams().get(index++).toString()));
	           bq.add(new BooleanClause(tq, BooleanClause.Occur.MUST));
	        }
	
	        FullTextQuery query = fullTextEm.createFullTextQuery(bq, model.getClass());
	        
	    	pageInfo = new PageInfo(config.getPageSize());
	    	if (qlInfo.getMaxResults() != 0) {
				// pager query
				query.setFirstResult(qlInfo.getFirstResult());
				query.setMaxResults(qlInfo.getMaxResults());
	
				List<T> result = query.getResultList();
				int size = qlInfo.getMaxResults();
				pageInfo.setPageSize(size);
				pageInfo.setPageNoByRowNo(qlInfo.getFirstResult());
				pageInfo.setData(result);
			} else {
				// query all data
				List<T> result = query.getResultList();
				int size = result == null? 0: result.size();
				pageInfo.setPageSize(size);
				pageInfo.setTotalCount(size);
				pageInfo.setData(result);
			}
    	} else {
    		logger.debug("parameter 'model' is null");
    	}
    	
    	if (pageInfo == null) {
    		pageInfo = new PageInfo<T>(config.getPageSize());
    		pageInfo.setData(new ArrayList<T>(0));
    	}
    	return pageInfo;
    }
    
    /**
     * search all the data result
     * 
     * @param model
     * @return
     */
    public <T> PageInfo<T> searchAll(@GenericType T model) {
    	QLInfo qlInfo = qlParse.getFromClause(model, Condition.DEFAULT);
    	// TODO: use lazy collection to improve the performance
    	PageInfo<T> pageInfo = searchByQl(qlParse.toSelectHQL(qlInfo), false);
    	return pageInfo;
    }
    
    /**
     * calculate the record count
     * 
     * @param qlInfo
     * @return
     */
    @Protected
    public int getCount(QLInfo qlInfo, boolean cacheable) {
    	addPermissionFilter(qlInfo);
    	String ql = qlInfo.getQl();
    	int begin = 0;
    	if (ql.startsWith("select a ")) {
    		begin = 9;
    	}
		int end = ql.lastIndexOf(" order by ");
		if (end == -1) {
			end = ql.length();
		}
		if (begin != 0 || end > 0) {
			ql = ql.substring(begin, end);
		}
		
		// get the total count
		String countHql = "select count(a) " + ql;		
		if (logger.isDebugEnabled()) {
			String oldHql = qlInfo.getQl();
			qlInfo.setQl(countHql);
			logger.debug(qlInfo);
			qlInfo.setQl(oldHql);
		}
		
		Query query = em.createQuery(countHql);
		if (cacheable) {
			query.setHint("org.hibernate.cacheable", true);
		}
		if (qlInfo.getParams() != null) {
			int index = 1;
    		for (Object value : qlInfo.getParams()) {
				query.setParameter(index++, value);
			}
    	}
		
		Object value = query.getSingleResult();
		int count = Integer.parseInt(value.toString());
		return count;
    }
    
    /**
     * search table data
     * 
     * @param qlInfo
     * @return
     */
    @Protected
    public PageInfo searchByQl(QLInfo qlInfo, boolean cacheable) {
    	addPermissionFilter(qlInfo);
    	String ql;
    	if (qlInfo.getSort() != null) {
    		ql = qlInfo.getQl() + " order by " + qlInfo.getSort() + ' ' + qlInfo.getDir();
    	} else {
    		ql = qlInfo.getQl();
    	}
    	if (!ql.startsWith("select a ")) {
    		ql = "select a " + ql;
    	}
    	Query query = em.createQuery(ql);
    	if (cacheable) {
    		query.setHint("org.hibernate.cacheable", true); 
    	}
    	if (qlInfo.getParams() != null) {
    		int index = 1;
    		for (Object value : qlInfo.getParams()) {
				query.setParameter(index++, value);
			}
    	}

    	PageInfo pageInfo = new PageInfo(config.getPageSize());
    	if (qlInfo.getMaxResults() != 0) {
			// pager query
			query.setFirstResult(qlInfo.getFirstResult());
			query.setMaxResults(qlInfo.getMaxResults());

			List<?> result = query.getResultList();
			int size = qlInfo.getMaxResults();
			pageInfo.setPageSize(size);
			pageInfo.setPageNoByRowNo(qlInfo.getFirstResult());
			pageInfo.setTotalCount(result.size());	// Don't get real the total count
			pageInfo.setData(result);
		} else {
			// query all data
			List<?> result = query.getResultList();
			int size = result == null || result.size() == 0? 1: result.size();
			pageInfo.setPageSize(size);
			pageInfo.setPageNoByRowNo(qlInfo.getFirstResult());
			pageInfo.setTotalCount(size);
			pageInfo.setData(result);
		}
    	return pageInfo;
    }
    
    public <T> List<T> addList(@GenericType List<T> model) {
    	List<T> result = new ArrayList<T>(model.size());
    	for (T theModel : model) {
    		result.add(add(theModel));
    	}
    	return result;
    }
    
    public <T> T add(@GenericType T model) {
    	if (load(model) != null) {
    		throw new AppException("errors.duplicate", getId(model));
    	}
    	
    	if (model != null) {
    		T newModel = em.merge(model);
    		em.flush();
    		model = newModel;
    	}
    	return model;
    }
    
    @Protected
    public <T> T merge(T model) {
        // update
    	T newModel = em.merge(model);
    	em.flush();
    	return newModel;
    }
    
	private Object getId(Object model) {
		Object id = ognlUtil.getValue("id", BlankObject.Map, model);
		return id;
    }
    
    private void setId(Object model, Object id) {
		ognlUtil.setValue("id", BlankObject.Map, model, id);
    }
    
    
    public void updateList(@GenericType List<Object> model) {
    	if (model != null) {
	    	for (Object theModel : model) {
	    		update(theModel);
	    	}
    	}
    }
    
    public void update(@GenericType Object model) {
    	Object id = getId(model);
    	if (id == null) {
    		throw new AppException("error.missId");
    	}
    	Object existed = em.find(model.getClass(), id);
    	if (existed == null) {
    		throw new AppException("error.missModel");
    	}
    	
    	if (model != existed) {
    		//HttpServletRequest request = ServletActionContext.getRequest();
    		if (ActionContext.getContext() != null){
	    		ValueStack stack = ActionContext.getContext().getValueStack();
	    		PageElements ps = (PageElements) stack.getContext().get(FrameConstant.STACK_PAGEELEMENTS);
	    		if (ps != null) {
	    			List<String> inputFields = ps.getModelFields();
		    		Set<String> fieldMap = new HashSet<String>(inputFields.size() * 4 / 3);
		    		for (String fieldName : inputFields) {
		    			int begin = fieldName.indexOf('.') + 1;		// skip the first prefix: 'model.'
		    			int next = fieldName.length();
		    			do {
		    				String name = fieldName.substring(begin, next);
		    				fieldMap.add(name);
		    				next = fieldName.lastIndexOf('.', next - 1);
		    			} while (next > begin);
		    		}
		    		ognlUtil.deepUpdate(model, existed, fieldMap);
	    		}
    		}
    	}
    	
        // update
    	em.merge(existed);
    	em.flush();
    }
    
    public boolean deleteList(@GenericType List<Object> results) {
    	boolean ret = true;
    	if (results != null) {
			for (Object theModel : results) {
				if (theModel != null) {
					ret &= delete(theModel);
				}
			}
    	}
    	return ret;
    }

    public boolean delete(@GenericType Object model) {
    	// TODO: check the model id?
    	if (model != null) {
    		model = load(model);
    		if (model != null) {
    			em.remove(model);
    			em.flush();
    			return true;
    		}
    	}
    	return false;
    }
    
    public Object _update(@GenericType Object model) {
    	return view(model);
    }
    
    /**
     * load the model object and update the result to input parameter 
     */
    public <T> T view(@GenericType T model) {
    	Object dbModel = load(model);
    	if (model instanceof Map) {
    		((Map)model).putAll((Map)dbModel);
    	} else {
    		ognlUtil.getOgnlUtil().copy(dbModel, model, BlankObject.Map);
    	}
    	return model;
    }
    
    @SuppressWarnings("unchecked")
	@Protected
    public <T> T load(T model) {
    	Object id = getId(model);
    	T ret;
    	if (id != null && id instanceof Serializable ) {
    		String className;
            if (model instanceof Map) {	// JPA doesn't support the dynamic model
            	className = (String)((Map)model).get(IDynamicModel.ENTITY_NAME);
            	Query query = em.createQuery(" from " + className + " where id=?");
          		query.setParameter(1, id);
                ret = (T)query.getSingleResult();
            } else {
            	ret = (T)em.find(model.getClass(), id);
            }
    	} else {
    		ret = null;
    	}
    	return ret;
    }
    
    // for com.googlecode.cswish.struts.BaseStrutsTestCaseTest
    public void test(Object model) {}
}